# -*- encoding: utf8 -*-
# vim: ai ts=4 sts=4 et sw=4

import re
import HTMLParser
import urllib2
import urlparse
import urllib
import random
import sys
import datetime
import os
import ConfigParser as configparser

EXPERIMENTAL=('2.3', '3')
VSPATCHESURL='http://vserver.13thfloor.at/Experimental/'
GRPATCHESURL='http://people.linux-vserver.org/~harry/'
#TEMPLATEPAGE='http://linux-vserver.org/index.php?title=Template:MyTestTemplate&action=edit'
TEMPLATEPAGE='http://linux-vserver.org/index.php?title=Template:ExperimentalPatchTableMatrix&action=edit'
UVTEMPLATEPAGE='http://linux-vserver.org/index.php?title=Template:UtilVServerTableMatrix&action=edit'
STABLEUVURL='http://ftp.linux-vserver.org/pub/utils/util-vserver/'
TESTINGUVURL='http://people.linux-vserver.org/~dhozac/t/uv-testing/'
TEMPLATEEDITFORM='editform'
# set this if there are more <textarea> fields on the <form>
#TEMPLATEEDITFIELD='wpTextbox1'
LOGINURL='http://linux-vserver.org/index.php?title=Special:Userlogin'
LOGOUTURL='http://linux-vserver.org/index.php?title=Special:Userlogout'
LOGINFORM='userlogin'
SUBMITLOGINFORM='wpLoginAttempt'
SUBMITEDITFORM='wpSave'
# set this if there are more <input type="text"...> fields on the <form>
#SUMMARYFIELD = 'wpSummary'
# set this for custom summary
SUMMARY=''
USERFIELD='wpName'
PASSFIELD='wpPassword'
#USERNAME=''
#PASSWORD=''

class Stripper(HTMLParser.HTMLParser):
    def __init__(self, expression):
        self.meat = []
        self.expression = re.compile(expression)
        HTMLParser.HTMLParser.__init__(self)

    def handle_data(self, data):
        if self.expression.match(data):
            self.meat.append([self.expression.findall(data)[0], data])

class FormParser(HTMLParser.HTMLParser):
    """Take html text, find <form> in it, and return list of forms
       on the page converted to F() objects"""
    class F(object):
        """Store for <form>s being parsed"""
        def __init__(self):
            self.method = None
            self.action = None
            self.name = None
            self.hidden = {}
            self.submit = {}
            self.text = []
            self.textarea = []

    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.forms = []

    def handle_starttag(self, tag, attrs):
        """This hopes for valid html and neting tags"""
        a = dict(attrs)
        if tag == 'form':
            self.forms.append(self.F())
            self.forms[-1].method = a.get('method')
            self.forms[-1].action = a.get('action')
            self.forms[-1].name = a.get('name')
        elif tag == 'textarea':
            self.forms[-1].textarea.append(a['name'])
        elif tag == 'input':
            t = a.get('type')
            if t == 'hidden':
                self.forms[-1].hidden[a.get('name')]=a.get('value')
            elif t == 'submit':
                self.forms[-1].submit[a.get('name')]=a.get('value')
            elif t == 'text' or t == None:
                self.forms[-1].text.append(a.get('name'))


class Session(object):
    def __init__(self, opener):
        self.boundary = '---------------------------' + ''.join([ random.choice('0123456789') for i in range(29) ])
        self.body = {}
        self.content_type = 'multipart/form-data; boundary=' + self.boundary
        self.form = None
        self.opener = opener
        self.parser = None
        self.url = None

    def get(self, url):
        # url: string or urllib2.Request object
        try:
            u = self.opener.open(url)
            c = u.read()
        except urllib2.URLError:
            return None

        u.close()
        return c

    def fetch_and_parse(self, url, parser=None):
        # url: string or urllib2.Request object
        # parser: parser
        if parser:
            self.parser = parser
        elif self.parser:
            self.parser.reset()

        if isinstance(url, basestring):
            self.url = url

        self.body = {}

        if self.parser:
            c = self.get(url)
            if c:
                self.parser.feed(c)
                return c

    def extract_form(self, form_name):
        # form parser will create parser.forms list of dicts
        if self.parser and self.parser.forms:
            for f in self.parser.forms:
                if f.name == form_name:
                    self.form = f
                    break
            self.method = f.method if f.method else 'post'
            us = urlparse.urlsplit(self.url)
            self.action = us.scheme + "://" + us.netloc + f.action if f.action.startswith("/") else us.path + f.action
            
    def as_multipart(self):
        body = []
        for k,v in self.body.items():
            body.append('--' + self.boundary)
            body.extend(['Content-Disposition: form-data; name="%s"' % k, ''])
            body.extend(v)
        body.append('--' + self.boundary + '--')
        return '\r\n'.join(body)

    def as_request(self):
        params = {}
        for field,values in self.body.items():
            params[field]='\r\n'.join(values)
        return urllib.urlencode(params)

    def get_request_for_form(self):
        if self.action:
            return urllib2.Request(self.action, self.as_request())
        else:
            return None

    def add_part(self, field, value=None):
        """field: field to (re)create
           value: optional value for the field
        """
        if value == None:
            self.body[field]=[]
        else:
            self.body[field]=[value]

    def add_lines(self, field, lines):
        """field: key to add to or create if doesn't exist
           lines: list of strings to append
        """
        if self.body.get(field):
            self.body[field].extend(lines)
        else:
            self.body[field] = lines

    def reset(self):
        self.boundary = '---------------------------' + ''.join([ random.choice('0123456789') for i in range(29) ])
        self.body = {}
        self.content_type = 'multipart/form-data; boundary=' + self.boundary
        self.form = None
        

    def add_hidden_fields(self):
        if self.form:
            for k,v in self.form.hidden.items():
                self.add_part(k, v)

def login(url, form=None, button=None, username=None, password=None, user_field=None, pass_field=None):
    # we need to know all the fields
    if None in locals().values():
        return None
    # session is cookie based, so prepare cookie-aware opener 
    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())
    # fetch a login form
    c = Session(opener)
    c.fetch_and_parse(url, FormParser())
    c.extract_form(form)

    # populate fields
    c.add_part(user_field, username)
    c.add_part(pass_field, password)
    c.add_part(button, c.form.submit[button])
    # populate hidden fields
    c.add_hidden_fields()

    login_form_request=c.get_request_for_form()
    if login_form_request and c.get(login_form_request):
        return c
    else:
        return None

def build_dictionary(meat, limits=EXPERIMENTAL):
    """meat: list of lists in form [(full_kern_ver, kernel.ver.sion, full_vs_ver), filename]
       limits: string against which compare vs_vers
    """
    l=[]
    keys={}
    ml = len(meat)
    for d in meat:            # for every list / tuple
        seen = set()
        k={}
        for item in d:
            if item[0][2].startswith(limits):      # if item for latest(*) vs version
                if item[0][1] not in seen:         # if it's the first appearance for a given kernel
                    seen.add(item[0][1])           # we've already seen this kernel
                    k[item[0][0]]=item[1]          # but the item is for specific kernel release
                    keys[item[0][0]]=[None]*ml     # placeholder for every passed list
                                                   # TODO: imporve it further, so we don't create values
                                                   # multiple times for given key
        l.append(k)

    i=0
    for d in l:
        for item in d:
            keys[item][i] = d[item]
        i+=1

    return keys

def sort_by_int(arg):
    if isinstance(arg, basestring):
        cutter = re.compile(r'\d+')
        return map(int, cutter.findall(arg))
    else:
        return None

def latest(meat, depth=4):
    """ take first depth numbers of version string, then take latest
        (highest in numeric sort)
    """
    if depth == 1:
        cutter = re.compile(r'\d+')
    elif depth > 1:
        #TODO: improve this regex and drop [0] later
        cutter = re.compile('\d+(?:\.\d+){1,%d}' % (depth-1))
    return max([ cutter.findall(i[0][2])[0] for i in meat], key=sort_by_int)


def experimental_patches_table():
    anon_session = Session(urllib2.build_opener())
    vers_regex = r'^patch-(((?:(?:2\.6)|3)\.\d+)(?:(?:\.|-rc)\d+)?)-vs((?:\d+)(?:\.\d+)*(?:[.-]pre\d+)?)'
    anon_session.fetch_and_parse(VSPATCHESURL, Stripper(vers_regex))
    vs_stuff = anon_session.parser.meat[:]

    anon_session.parser.meat = []
    anon_session.fetch_and_parse(GRPATCHESURL)

    latest_rel = EXPERIMENTAL if EXPERIMENTAL else latest(vs_stuff)

    table = build_dictionary([reversed(vs_stuff), reversed(anon_session.parser.meat)], latest_rel)

    lines = [
'{| class="wikitable" style="margin: 2em auto 2em auto; border-width: 0;"',
'! Linux-VServer branch',
'Linux kernel',
'!class="devel"| 2.3',
'Experimental',
'! 2.3 + grsecurity',
'Experimental',
    ]

    ver_cutter = re.compile(r"-(vs\d+.*).diff")
    kern_cutter = re.compile(r'(^\d+\.\d+).*')
    for k in sorted(table.keys(), reverse=True, key=sort_by_int):
        kline = kern_cutter.findall(k)[0]
        kaddpath = ""
        if kline.startswith("3"):
            kline = "3.x"
        if kline.startswith("2.6"):
            kver = map(int, k.split('.'))
            if kver[2] in (22, 26):
                continue
            elif (kver[2] == 27 and (kver[3] > 57 and kver[3] < 63)) or \
                 (kver[2] == 32 and (kver[3] > 27 and kver[3] < 62)) or \
                 (kver[2] == 33 and (kver[3] > 7 and kver[3] < 21)) or \
                 (kver[2] == 34 and (kver[3] > 7 and kver[3] < 16)) or \
                 (kver[2] == 35 and (kver[3] > 9 and kver[3] < 15)):
                    kaddpath = "/longterm/v" + str(kver[0]) + '.' + str(kver[1]) + '.' + str(kver[2])
        lines.append('|-')
        lines.append("| [http://www.kernel.org/pub/linux/kernel/v" + kline + kaddpath + "/linux-" + k + ".tar.gz " + k + "]")
        lines.append("| [" + VSPATCHESURL + table[k][0] + " " + ver_cutter.findall(table[k][0])[0] + "]" if table[k][0] else "|")
        lines.append("| [" + GRPATCHESURL + table[k][1] + " " + ver_cutter.findall(table[k][1])[0] + "]" if table[k][1] else "|")
#    lines.append('|-')
#    lines.append('|style="font-size: 70%; text-align: right; background: #fff; border-width: 0;" colspan="3"| ~~~~~')
    lines.append('|}')

    return lines

def util_vservers_table():
    anon_session = Session(urllib2.build_opener())
    anon_session.fetch_and_parse(STABLEUVURL, Stripper(r'util-vserver-((?:\d+)(?:\.\d+)*).tar.(?:bz2|xz)$'))
    stable_uv = dict(anon_session.parser.meat)

    anon_session.parser.meat = []
    anon_session.fetch_and_parse(TESTINGUVURL, Stripper(r'util-vserver-((?:\d+)(?:\.\d+)*(?:-pre\d+))'))
    testing_uv = dict(anon_session.parser.meat)

    lines = [
'{|class="wikitable" style="margin: 2em auto 2em auto;"',
'! !! util-vserver',
'|-',
'! stable ',
    ]

    latest_stable = sorted(stable_uv, key=sort_by_int)[-1]
    latest_testing = sorted(testing_uv, key=sort_by_int)[-1]
    
    lines.append('| [%s%s %s]' % (STABLEUVURL, stable_uv[latest_stable], latest_stable))
    lines.extend(['|-', '! testing'])
    lines.append('| [%s%s %s]' % (TESTINGUVURL, testing_uv[latest_testing], latest_testing))
    lines.append('|}')

    return lines

def send_updated_form(wiki_session, templatepage, new_contents):
    c=wiki_session.fetch_and_parse(templatepage, FormParser())
    wiki_session.extract_form(TEMPLATEEDITFORM)
    wiki_session.add_hidden_fields()

    if len(wiki_session.form.textarea) == 1:
        edit_field = wiki_session.form.textarea[0]
    elif TEMPLATEEDITFIELD:
        edit_field = TEMPLATEEDITFIELD
    else:
        return None

    if len(wiki_session.form.text) == 1:
        summary_field = wiki_session.form.text[0]
    elif SUMMARYEDITFIELD:
        summary_field = SUMMARYEDITFIELD
    else:
        summary_field = None

    if summary_field:
        wiki_session.add_part(summary_field, SUMMARY if SUMMARY else 'automatic update by a script')
    # press the right button
    wiki_session.add_part(SUBMITEDITFORM, wiki_session.form.submit[SUBMITEDITFORM])

    wiki_session.add_lines(edit_field, new_contents)

    # create custom multipart request for now, and improve
    # Session class later, so we have an option to send form
    # in request or multipart.

    req = urllib2.Request(wiki_session.action, wiki_session.as_multipart())
    req.add_header('Content-type', wiki_session.content_type)
    return wiki_session.get(req)


if __name__ == '__main__':
    # try to read ~/.autotab
    # you can / should use this file to override / set credentials.
    # this file has windows' .ini - like syntax:
    # [cred]
    # username = put_username_here
    # password = and_password_goes_here
    cp=configparser.ConfigParser()
    cp.read(os.path.expanduser('~/.autotab'))
    if 'cred' in cp.sections():
        opts = dict(cp.items('cred'))
        if 'username' in opts:
            USERNAME = opts['username']
        if 'password' in opts:
            PASSWORD = opts['password']

    wiki_session = login(LOGINURL, LOGINFORM, SUBMITLOGINFORM, USERNAME, PASSWORD, USERFIELD, PASSFIELD)
    if wiki_session:
        if len(sys.argv) == 1 or 'patches' in sys.argv:
            send_updated_form(wiki_session, TEMPLATEPAGE, experimental_patches_table())
        elif 'util' in sys.argv:
            send_updated_form(wiki_session, UVTEMPLATEPAGE, util_vservers_table())
        wiki_session.get(LOGOUTURL)

